﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using SharpShare.Afp.Protocol;
using SharpShare.Afp.Protocol.Security;
using SharpShare.Collections;
using SharpShare.Diagnostics;
using SharpShare.Storage;
using System.IdentityModel.Tokens;

namespace SharpShare.Afp {
    public class AfpShareServer : ShareService, IAfpServer, ILogProvider {
        private ManualResetEvent _noSessionsEvent = new ManualResetEvent(true);
        private ConcurrentHashSet<AfpTransport> _connections = new ConcurrentHashSet<AfpTransport>();
        private Dictionary<IStorageProvider, AfpShareSettings> _shareSettings = new Dictionary<IStorageProvider, AfpShareSettings>();
        private AfpServerSettings _settings;
        private Socket _listenSocket;

        private Bonjour.DNSSDService _bonjourService = new Bonjour.DNSSDService();
        private Bonjour.DNSSDEventManager _bonjourEventManager = new Bonjour.DNSSDEventManager();
        private Bonjour.DNSSDService _bonjourRegistrar;
        private Bonjour.DNSSDService _bonjourTimeMachineRegistrar;

        public AfpShareServer(string name, AfpServerSettings settings = null)
            : base(name) {
            if (settings == null) {
                settings = new AfpServerSettings();
            }

            if (settings.AuthenticationMethods.Count == 0) {
                throw new ArgumentException("No user authentication methods (UAMs) provided in settings.", "settings");
            }

            _settings = settings.Clone();

            if (_settings.ServerSignature == Guid.Empty) {
                // Create server signature by hashing machine name.
                string digest = System.Environment.MachineName;
                SHA256Cng sha = new SHA256Cng();
                byte[] hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(digest));
                Array.Resize(ref hash, 16);

                _settings.ServerSignature = new Guid(hash);
            }
        }

        public AfpServerSettings Settings {
            get {
                return _settings.Clone();
            }
        }

        public AfpShareSettings GetShareSettings(IStorageProvider share) {
            return _shareSettings[share];
        }

        public string[] Versions {
            get { return new[] { /* "AFP3.3", "AFP3.4", "AFPX03"*/ "AFP3.3", "AFP3.4", "AFPX03" }; }
        }

        private ReadOnlyCollection<IAfpSession> AfpSessions {
            get {
                return this.Sessions.Cast<IAfpSession>().ToList().AsReadOnly();
            }
        }

        protected override void StartCore() {

            _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try {
                _listenSocket.Bind(new IPEndPoint(this.Settings.Binding, this.Settings.Port));
                _listenSocket.Listen(10);

                this.State = ShareServiceState.Running;
                _listenSocket.BeginAccept(ConnectionReceived, null);
            } catch {
                _listenSocket = null;
                throw;
            }

            try {
                this.PublishBonjourServices();
            } catch (Exception ex) {
                Log.Add(this, EntryType.Error, "Unable to publish Bonjour services: {0}", ex);
            }

            Log.Add(this, EntryType.Information, "AFP server listening on port {0}.", this.Settings.Port);
        }

        protected override void StopCore() {
            this.Shutdown();

            _listenSocket.Dispose();
            _listenSocket = null;

            try {
                this.UnpublishBonjourServices();
            } catch (Exception ex) {
                Log.Add(this, EntryType.Error, "Exception unpublishing Bonjour services: {1}", ex);
            }

            foreach (IAfpSession session in this.AfpSessions) {
                try {
                    session.Notify(AfpUserBytes.ShutDownInMinutes, 0);
                } catch (Exception ex) {
                    Log.Add(this, EntryType.Error, "Exception sending shut down notification to session '{0}': {1}.", session, ex);
                }
            }

            foreach (IShareSession session in this.Sessions) {
                session.Close();
            }

            Log.Add(this, EntryType.Information, "AFP server stopped.");
        }

        private void Shutdown() {
            this.Shutdown(this.Settings.ShutdownDelay);
        }

        private bool Shutdown(TimeSpan delay) {
            int minutes = (int)Math.Ceiling(delay.TotalMinutes);
            if (minutes <= 0) {
                throw new ArgumentException("Must be greater than or equal to 1 minute.", "delay");
            }

            try {
                BroadcastNotification(AfpUserBytes.ShutDownInMinutes, (ushort)minutes);
            } catch (Exception ex) {
                Log.Add(this, EntryType.Error, "Exception while broadcasting shut down notification: {0}", ex);
            }

            while (delay > TimeSpan.Zero) {
                bool signaled = _noSessionsEvent.WaitOne(TimeSpan.FromSeconds(1.0));

                if (signaled) {
                    break;
                }

                foreach (IAfpSession session in this.AfpSessions.Where(s => !s.HasTransport)) {
                    session.Close();
                }

                delay -= TimeSpan.FromSeconds(1.0);
            }

            bool everyoneDisconnected = (this.Sessions.Count == 0);

            return everyoneDisconnected;
        }

        public void BroadcastNotification(AfpUserBytes bytes, ushort argument) {
            foreach (IAfpSession session in this.AfpSessions) {
                session.Notify(bytes, argument);
            }
        }

        #region Transport Event Handlers

        private void ConnectionReceived(IAsyncResult ar) {
            if (_listenSocket == null) {
                return;
            }

            try {
                Socket newSocket = _listenSocket.EndAccept(ar);

                if (this.State != ShareServiceState.Running) {
                    Log.Add(this, EntryType.Information, "Incoming connection from remote endpoint '{0}' closed, not running.", newSocket.RemoteEndPoint);
                    newSocket.Close();
                    return;
                }

                Log.Add(this, EntryType.Information, "Connection received from remote endpoint '{0}'.", newSocket.RemoteEndPoint);

                AfpTransport transport = new AfpTransport(newSocket);
                _connections.Add(transport);

                transport.CommandReceived += CommandReceived;
            } catch (Exception ex) {
                Log.Add(this, EntryType.Error, "Exception handling incoming connection: {0}", ex);
            }

            if (this.State == ShareServiceState.Running) {
                _listenSocket.BeginAccept(ConnectionReceived, null);
            }
        }

        private void CommandReceived(object sender, AfpTransportCommandReceivedEventArgs eventArgs) {
            AfpTransport transport = (AfpTransport)sender;

            try {
                if (this.State != ShareServiceState.Running) {
                    transport.SendReply(eventArgs.Header, AfpResultCode.FPServerGoingDown, new byte[0]);
                    return;
                }

                switch (eventArgs.Header.command) {
                    case DsiCommand.GetStatus:
                        AfpStream statusStream = new AfpStream();

                        transport.SendReply(eventArgs.Header, AfpResultCode.FPNoErr, this.WriteServerInfo());

                        break;
                    case DsiCommand.OpenSession:
                        transport.CommandReceived -= CommandReceived;

                        IAfpSession session = new AfpShareSession(this, transport);

                        AfpStream stream = new AfpStream(eventArgs.Payload);

                        while (!stream.EOF) {
                            Protocol.AfpOpenSessionOptions option = stream.ReadEnum<Protocol.AfpOpenSessionOptions>();
                            byte length = stream.ReadUInt8();
                            AfpStream optionStream = new AfpStream(stream.ReadBytes(length));

                            switch (option) {
                                case AfpOpenSessionOptions.kAttentionQuanta:
                                    session.AttentionQuantum = optionStream.ReadUInt32();
                                    break;
                            }

                            stream.ReadPadding();
                        }

                        _noSessionsEvent.Reset();

                        OnSessionCreated((IShareSession)session);

                        session.SessionClosed += new EventHandler(SessionClosed);

                        bool isFromLan = true;

                        try {
                            isFromLan = AfpTransport.IsLanAddress(((IPEndPoint)transport.Socket.RemoteEndPoint).Address);
                        } catch { }

                        Log.Add(this, EntryType.Information, "Session '{0}' opened ({1}).", session, (isFromLan ? "LAN" : "WAN"));

                        AfpStream sessionStream = new AfpStream();
                        sessionStream.WriteEnum(Protocol.AfpOpenSessionOptions.kRequestQuanta);
                        sessionStream.WriteUInt8(4); // Length
                        sessionStream.WriteUInt32((uint)(isFromLan ? this.Settings.RequestQuanta : this.Settings.WanRequestQuanta));

                        sessionStream.WritePadding();

                        sessionStream.WriteEnum(Protocol.AfpOpenSessionOptions.kServerReplayCacheSize);
                        sessionStream.WriteUInt8(4); // Length
                        sessionStream.WriteUInt32((uint)AfpReplyCache.ReplayCacheSize);

                        transport.SendReply(eventArgs.Header, AfpResultCode.FPNoErr, sessionStream.ToByteArray());

                        break;
                    case DsiCommand.Command:
                        throw new InvalidOperationException("Command without session.");
                    default:
                        throw new InvalidOperationException("Unknown DSI command.");
                }
            } catch (Exception ex) {
                Log.Add(this, EntryType.Error, "Exception handling incoming command from transport '{0}': {1}", transport, ex);

                transport.Close();
            }
        }

        #endregion

        #region Session Event Handlers

        private void SessionClosed(object sender, EventArgs e) {
            IAfpSession session = (IAfpSession)sender;

            OnSessionEnded((IShareSession)session);

            if (this.Sessions.Count == 0) {
                _noSessionsEvent.Set();
            }
        }

        #endregion

        private byte[] WriteServerInfo() {
            AfpServerFlagsBitmap serverFlags =
                AfpServerFlagsBitmap.kSupportsSrvrMsg |
                AfpServerFlagsBitmap.kSupportsSrvrSig |
                AfpServerFlagsBitmap.kSupportsTCP |
                AfpServerFlagsBitmap.kSupportsSrvrNotify |
                AfpServerFlagsBitmap.kSupportsReconnect |
                AfpServerFlagsBitmap.kSupportsDirServices |
                AfpServerFlagsBitmap.kSupportsCopyfile |
                AfpServerFlagsBitmap.kSupportsExtSleep |
                AfpServerFlagsBitmap.kSupportsUUIDs |
                AfpServerFlagsBitmap.kSupportsUTF8SrvrName;

            AfpShareServer server = this;
            AfpStream responseStream = new AfpStream();
            List<string> uams = this.Settings.AuthenticationMethods.Select(m => m.Name).ToList();

            if (uams.Count == 0) {
                uams.Add(AfpUams.kNoUserAuthStr);
            }

            responseStream.BeginMarking();

            responseStream.WriteMark("MachineType");
            responseStream.WriteMark("AFPVersionsCount");
            responseStream.WriteMark("UAMCount");
            responseStream.WriteMark("VolumeIconAndMask");

            responseStream.WriteEnum(serverFlags);
            responseStream.WritePascalString(server.Name);
            responseStream.WritePadding();

            responseStream.WriteMark("ServerSignature");
            responseStream.WriteMark("NetworkAddressesCount");
            responseStream.WriteMark("DirectoryNamesCount");
            responseStream.WriteMark("UTF8ServerName");

            responseStream.BeginMark("MachineType");
            responseStream.WritePascalString(this.Settings.MachineType);

            responseStream.BeginMark("AFPVersionsCount");
            responseStream.WriteUInt8((byte)server.Versions.Length);
            foreach (string version in server.Versions) {
                responseStream.WritePascalString(version);
            }

            responseStream.BeginMark("UAMCount");
            responseStream.WriteUInt8((byte)uams.Count);
            foreach (string uam in uams) {
                responseStream.WritePascalString(uam);
            }

            responseStream.BeginMark("ServerSignature");
            responseStream.WriteBytes(server.Settings.ServerSignature.ToByteArray());

            responseStream.BeginMark("NetworkAddressesCount");
            responseStream.WriteUInt8(0);

            responseStream.BeginMark("DirectoryNamesCount");
            responseStream.WriteUInt8(0);

            responseStream.BeginMark("VolumeIconAndMask");
            responseStream.WriteBytes(new byte[256]);

            responseStream.BeginMark("UTF8ServerName");
            responseStream.WriteUTF8String(this.Name);

            responseStream.EndMarking();

            return responseStream.ToByteArray();
        }

        #region IAfpServer Members

        IAfpUserManager IAfpServer.UserManager {
            get {
                return this.Settings.UserManager;
            }
        }

        IList<IStorageProvider> IAfpServer.Shares {
            get { return this.Shares; }
        }

        string IAfpServer.LoginMessage { get; set; }

        IAfpSession IAfpServer.FindSession(AfpToken token, AfpSessionSearchType type) {
            IAfpSession session = null;

            if (type == AfpSessionSearchType.ClientIssued) {
                var matching = this.AfpSessions
                    .Where(s => s.ClientToken.HasValue)
                    .Where(s => s.ClientToken.Value == token);

                session = matching.FirstOrDefault();
            } else {

                var matching = this.AfpSessions
                    .Where(s => s.ServerToken.HasValue)
                    .Where(s => s.ServerToken.Value == token);

                session = matching.FirstOrDefault();
            }

            return session;
        }

        IAfpUserAuthenticationMethod IAfpServer.GetAuthenticationMethod(string name) {
            return this.Settings.AuthenticationMethods.Where(m => m.Name == name).FirstOrDefault();
        }

        AfpResultCode IAfpServer.HandleCommandException(Exception ex) {
            AfpResultCode? code = GetExceptionResultCode(ex.GetType());
            if (!code.HasValue) {
                throw new InvalidOperationException("Unable to handle exception.", ex);
            }

            return code.Value;
        }

        #endregion

        #region ILogProvider Members

        string ILogProvider.Name { get { return "AFP Server"; } }

        #endregion

        #region Bonjour

        private void PublishBonjourServices() {
            _bonjourRegistrar = _bonjourService.Register(0, 0, this.Name, "_afpovertcp._tcp", null, null, (ushort)this.Settings.Port, null, _bonjourEventManager);

            Bonjour.TXTRecord record = this.CreateTimeMachineTXTRecord();
            _bonjourTimeMachineRegistrar = _bonjourService.Register(0, 0, this.Name, "_adisk._tcp", null, null, (ushort)this.Settings.Port, record, _bonjourEventManager);
        }
        private void UnpublishBonjourServices() {
            _bonjourRegistrar = _bonjourTimeMachineRegistrar = null;

            //_bonjourService.Stop();

            //if (_bonjourRegistrar != null) {
            //    _bonjourRegistrar.Stop();
            //    _bonjourRegistrar = null;
            //}

            //if (_bonjourTimeMachineRegistrar != null) {
            //    _bonjourTimeMachineRegistrar.Stop();
            //    _bonjourTimeMachineRegistrar = null;
            //}
        }
        private Bonjour.TXTRecord CreateTimeMachineTXTRecord() {
            Bonjour.TXTRecord timeMachineRecord = new Bonjour.TXTRecord();
            for (int x = 0; x < this.Shares.Count; x++) {
                IStorageProvider share = this.Shares[x];
                AfpShareSettings shareSettings = this.GetShareSettings(share);
                string key = string.Format("tmdisk{0}", x);
                StringBuilder value = new StringBuilder();

                Dictionary<string, string> subvalues = new Dictionary<string, string>();
                subvalues["adVN"] = share.Name;

                if (shareSettings.TimeMachineEnabled) {
                    subvalues["adVF"] = "0x81";
                } else {
                    subvalues["adVF"] = "0x01";
                }

                foreach (string subKey in subvalues.Keys) {
                    if (value.Length > 0) {
                        value.Append(",");
                    }

                    string escapedValue = subvalues[subKey]
                        .Replace('\\', (char)0)
                        .Replace(",", "\\,")
                        .Replace(new string((char)0, 1), "\\\\");

                    value.Append(subKey);
                    value.Append("=");
                    value.Append(escapedValue);
                }

                // timeMachineRecord.SetValue(key, Encoding.ASCII.GetBytes(value.ToString()));
                timeMachineRecord.SetValue(key, value.ToString());
            }

            return timeMachineRecord;
        }


        #endregion

        protected override void OnShareAdded(IStorageProvider share) {
            _shareSettings[share] = new AfpShareSettings();
        }

        protected override void OnShareRemoved(IStorageProvider share) {
            _shareSettings.Remove(share);
        }

        #region Static
        private static List<Tuple<Type, AfpResultCode>> _exceptionTypeResultCodeMap = new List<Tuple<Type, AfpResultCode>>();

        static AfpShareServer() {
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageFileConflictException), AfpResultCode.FPDenyConflict));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageItemNotFoundException), AfpResultCode.FPObjectNotFound));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageContainerNotFoundException), AfpResultCode.FPDirNotFound));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageItemUnauthorizedException), AfpResultCode.FPAccessDenied));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageItemLockedException), AfpResultCode.FPObjectLocked));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageItemInUseException), AfpResultCode.FPFileBusy));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageContainerFullException), AfpResultCode.FPDiskFull));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageContainerNotEmptyException), AfpResultCode.FPDirNotEmpty));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageItemAlreadyExistsException), AfpResultCode.FPObjectExists));
            _exceptionTypeResultCodeMap.Add(Tuple.Create(typeof(StorageItemInvalidTypeException), AfpResultCode.FPObjectTypeErr));
        }

        private static AfpResultCode? GetExceptionResultCode(Type exType) {
            AfpResultCode? code = _exceptionTypeResultCodeMap
                .Where(m => exType.IsAssignableFrom(m.Item1))
                .Select(m => m.Item2)
                .Cast<AfpResultCode?>()
                .FirstOrDefault();

            return code;
        }

        #endregion

    }
}
